5. Object Relational Mapping (GORM)

도메인 클래스는 모든 비지니스 어플리케이션에서 핵심이다. 도메인 클래스에는 비지니스 프로세스의 상태를 유지시키고 행동을 구현한다. 도메인 클래스는 서로 일대일 혹은 일대다 관계를 이룬다.

GORM은 Grails에서 사용하는 객체 관계형 매핑 구현체이다. 내부적으로 Hibernate 3(아주 인기있고 유연한 오픈소스 ORM)를 이용하지만 Groovy의 동적인 성질과 관례 때문에 Grails의 도메인 클래스를 생성하는 것까지 포함하더라도 설정할게 별로 없다. 실제로 Grails는 정적 타이핑과 동적 타이핑을 모두 지원한다.

Grails 도메인 클래스는 자바로도 작성할 수 있다. Hibernate와 통합하는 장을 보면 자바로 Grails의 도메인 클래스를 작성하는 법을 확인할 수 있다. 그러나 여전히 동적인 영속성 메소드를 사용한다. 다음은 GORM을 활용하는 예이다:

def book = Book.findByTitle("Groovy in Action")

book .addToAuthors(name:"Dierk Koenig") .addToAuthors(name:"Guillaume LaForge") .save()

5.1 Quick Start Guide

create-domain-class 명령으로 도메인 클래스를 만든다:

grails create-domain-class Person

도메인 클래스는 grails-app/domain/Person.groovy 에 생성되며 그 내용은 다음과 같다:

class Person {	
}

DataSource에 dbCreate 프로퍼티가 “update”, “create”, “create-drop”중에 하나로 설정하면 Grails는 자동으로 데이터베이스 테이블을 생성하거나 수정한다.

다음과 같이 클래스에 몇 개의 프로퍼티를 추가한다:

class Person {	
	String name
	Integer age
	Date lastVisit
}

도메인 클래스를 만들고 shell이나 console에서 관리할 수 있다:

grails console

이제 Groovy 명령을 입력할 수 있는 GUI 프로그램이 실행된다.

5.1.1 Basic CRUD

기본적인 CRUD(Create/Read/Update/Delete) 명령을 사용해 보자.

Create

Groovy의 new 연산자를 사용하여 도메인 클래스를 생성하고 프로퍼티 값을 입력하고 save 메소드를를 호출한다:

def p = new Person(name:"Fred", age:40, lastVisit:new Date())
p.save()

save 메소드는 Hibernate을 이용하여 도메인 클래스를 영속화한다.

Read

Grails는 내부적으로 도메인 클래스에 id 프로퍼티를 추가한다.

def p = Person.get(1)
assert 1 == p.id

이 예에서는 get 메소드를 이용하여 데이터베이스에서 Person 객체를 읽는다.

Update

특정 인스턴스의 정보를 업데이트하기 위해서 필요한 프로퍼티들을 변경하고 단순하게 다시 save 메소드를 호출하면 된다:

def p = Person.get(1)
p.name = "Bob"
p.save()

Delete

delete 메소드로 인스턴스를 삭제한다:

def p = Person.get(1)
p.delete()

5.2 Domain Modelling in GORM

Grails 어플리케이션을 만들려면 해결할 문제의 도메인을 알아야 한다. Amazon 서점을 만들고 싶다면 책, 저자, 고객, 출판사들을 고려하게 될 것이다.

여기에서는 제목, 출판일, ISBN등의 정보를 가지는 Book 클래스를 GORM으로 모델링 할 것이다. 계속해서 GORM으로 도메인을 모델링하는 법을 보여줄 것이다.

create-domain-class 명령으로 도메인 클래스를 생성할 수 있다:

grails create-domain-class Book

이 명령을 실행하면 grails-app/domain/Book.groovy 파일이 다음과 같은 내용으로 생성된다:

class Book {	
}

패키지를 사용하려면 도메인 클래스의 위치를 도메인 디렉토리 밑의 하위 디렉토리로 옮기고 Groovy 패키지 규칙(Java와 동일하다)에 따라 패키지를 정의하면 된다.

위 클래스는 자동적으로 클래스 이름과 동일한 데이터베이스의 book 테이블과 매핑된다. 이 규칙은 ORM Domain Specific Language 도메인 명세 언어(ORM Domain Specific Language)로 수정할 수 있다.

이제 도메인 클래스를 만들었고 Java 데이터 형식을 이용하여 프로퍼티를 정의해보자. 다음의 예를 보면:

class Book {
	String title
	Date releaseDate
	String ISBN
}

각 프로퍼티는 데이터베이스의 컬럼으로 매핑되고 그 컬럼 이름은 '_'로 구분되는 소문자로 만들어진다. 예를 들어 releaseDate는 release_date의 컬럼이름으로 매핑된다. SQL 데이터 형식은 Java 데이터 형식에 따라 자동으로 매핑된다. 그리고 ORM DSL 이나 제약조건(Constraints)을 이용하여 매핑 규칙을 수정할 수 있다.

5.2.1 Association in GORM

관계(Relationships)는 도메인 클래스들이 상호 동작하는 방식을 의미한다. 양쪽 클래스에 정확하게 정의하지 않으면 관계는 정의된 쪽에서만 적용된다.

5.2.1.1 One-to-one

일대일 관계은 가장 단순형태다. 이 관계는 다른 도메인 클래스의 형식으로 프로퍼티를 정의하는 것만으로 쉽게 정의할 수 있다. 다음의 예를 보자:

Example A

class Face {
    Nose nose
}
class Nose {	
}

Face에서 Nose로의 단뱡향 일대일 관계를 만들었다. 이제 양뱡향 관계를 만들어보자:

Example B

class Face {
    Nose nose
}
class Nose {	
	Face face
}

이제 양뱡향 관계를 만들었다. 하지만 아직 두 클래스 모두 연쇄적으로 업데이트되지 않는다.

연쇄적으로 업데이트 되게하면 다음과 같다:

Example C

class Face {
    Nose nose
}
class Nose {	
	static belongsTo = [face:Face]
}

belongsTo를 이용하여 Nose를 Face에 종속시켰다. 이제 Face를 생성하고 저장하면 연쇄적으로 Nose도 추가되고 변경된다:

new Face(nose:new Nose()).save()

The example above will save both face and nose. Note that the inverse is not true and will result in an error due to a transient Face:

위 예제는 Face와 Nose가 모두 잘 저장되지만 Face가 비영속 객체(transient object)므로 역순으로 생성하면 저장이 안되고 오류가 발생할 것이다.

new Nose(face:new Face()).save() // will cause an error

그리고 Nose는 Face에 종속되어 있기 때문에 Face를 삭제하면 Nose 역시 삭제된다.

def f = Face.get(1)
f.delete() // both Face and Nose deleted

belongsTo를 명시하지 않았다면 연쇄적으로 삭제되지 않는다. Nose를 명시적으로 삭제하지 않은 상태에서 Face를 삭제하면 외래키 제약조건(foreign key constraint) 에러가 발생한다:

// error here without belongsTo
def f = Face.get(1)
f.delete()

// no error as we explicitly delete both def f = Face.get(1) f.nose.delete() f.delete()

다음과 같이 작성하면 이 관계를 단방향으로 만들 수도 있다. 그리고 연쇄적으로 저장, 업데이트 시킬 수 있다.

class Face {
    Nose nose
}
class Nose {	
	static belongsTo = Face
}

이 예제에서 belongsTo를 선언할 때 map 문법을 사용하지 않았고 관계(association)라고 부르지도 않았다는 점을 주목하자. Grails는 단방향으로 해석한다. 3가지 예제를 모두 요약하면 아래의 다이어그램과 같다.

5.2.1.2 One-to-many

일대다 관계는 하나의 클래스가 많은 다른 클래스의 인스턴스를 가지고 있는 것이다. AuthorBook 의 관계가 그렇다. hasMany를 이용하여 이런 관계를 정의한다.

class Author {
    static hasMany = [ books : Book ]

String name } class Book { String title }

우리는 단방향 1-n 관계를 만들었다. Grails는 기본적으로 Join 테이블로 이런한 관계를 매핑한다.

ORM DSL 은 외래키 관계을 사용하여 단방향 관계를 가능하게 한다.

Grails는 자동으로 도메인 클래스의 hasMany 가 명시된 프로퍼티들을 java.util.Set 형식으로 만든다. 따라서 컬렉션(collection)의 이터레이션(iteration)을 사용할 수 있다.

def a = Author.get(1)

a.books.each { println it.title }

Grails에서 사용하는 기본 패치(fetch) 전략은 필요할 때(lazily) 패치하는 "lazy" 전략이다. n+1 문제가 발생하지 않도록 조심해야 한다.

"eager" 패치 전략을 취하도록 query의 일부로서 ORM DSL 에 명시 할 수도 있다.

연쇄적으로 저장, 갱신하는 것이 기본적인 연쇄 방식이고 belongsTo가 명시되어 있지 않으면 연쇄적으로 삭제되지도 않는다.

class Author {
    static hasMany = [ books : Book ]

String name } class Book { static belongsTo = [author:Author] String title }

일대다 관계에서 '다'쪽의 도메인 클래스에 '일'쪽의 형식으로 정의된 프로퍼티가 두 개 이상 있다면 mappedBy를 사용하여 어떤 컬렉션에 매핑돼야 하는지 명시해야 한다.

class Airport {
	static hasMany = [flights:Flight]
	static mappedBy = [flights:"departureAirport"]
}
class Flight {
	Airport departureAirport
	Airport destinationAirport
}

또, 컬렉션을 어러개 만들어 '다'쪽의 도메인 클래스의 다른 프로퍼티로 매핑시키는 것도 가능하다.

class Airport {
	static hasMany = [outboundFlights:Flight, inboundFlights:Flight]
	static mappedBy = [outboundFlights:"departureAirport", inboundFlights:"destinationAirport"]
}
class Flight {
	Airport departureAirport
	Airport destinationAirport
}

5.2.1.3 Many-to-many

Grails는 다대다 관계를 지원한다. 양쪽 클래스에 hasMany를 정의하고 관계를 소유하는 쪽에 belongsTo를 사용하면 다대다 관계가 만들어진다.

class Book {
   static belongsTo = Author
   static hasMany = [authors:Author]
   String title
}
class Author {
   static hasMany = [books:Book]
   String name
}

Grails는 데이터베이스의 테이블 조인을 사용하여 다대다 관계를 구현한다. 관계를 소유하는 쪽에 관계를 영속화할 책임이 있다. 오직 한쪽에서만 연쇄적으로 저장할 수 있다. 이 경우에는 Author 클래스에 책임이 있다.

다음의 예는 올바르게 작동하고 연쇄적으로 저장된다:

new Author(name:"Stephen King")
		.addToBooks(new Book(title:"The Stand"))
		.addToBooks(new Book(title:"The Shining"))		
		.save()

하지만 아래의 예는 Book만 저장되고 Author들은 저장되지 않는다.

new Book(name:"Groovy in Action")
		.addToAuthors(new Author(name:"Dierk Koenig"))
		.addToAuthors(new Author(name:"Guillaume Laforge"))		
		.save()

다대다 관계에서는 오직 한 쪽에서만 관계를 관리할 수 있는데 이것은 Hibernate에서도 마찬가지다.

Grails의 Scaffolding 기능은 현재 다대다 관계를 지원하지 않는다. 따라서 관계를 관리하는 코드를 직접 작성해야 한다.

5.2.2 Composition in GORM

association 외에도 Grails는 결합(Composition)도 지원한다. 클래스를 각각의 테이블에 매핑하지 않고 하나의 테이블에 "포함하여" 매핑할 수 있다.

class Person {
	Address homeAddress
	Address workAddress
	static embedded = ['homeAddress', 'workAddress']
}
class Address {
	String number
	String code
}

다음과 같이 테이블이 매핑된다.

grails-app/domain 디렉토리에 새로운 Groovy 파일을 만들고 그 파일에 Address 클래스를 만들면 address 테이블이 생성된다. 이 것을 원하지 않으면 grails-app/domain/Person.groovy 파일의 Person 클래스 아래에 Address 클래스를 만든다. Groovy에서는 한 파일에 여러개의 클래스를 정의할 수 있다.

5.2.3 Inheritance in GORM

GORM 에서는 추상 부모 클래스와 영속 GORM 엔터티에서 상속받을 수 있다. 예를 들면:

class Content {
     String author
}
class BlogEntry extends Content {
    URL url
}
class Book extends Content {
    String ISBN
}
class PodCast extends Content {
    byte[] audioStream
}

이 예에서 우리는 부모 클래스인 Content 와 다른 특징을 가진 자식 클래스들을 만들었다.

Considerations(고려 사항)

Grails는 기본적으로 상속 구조당 하나의 테이블(table-per-hierarchy)에 매핑한다. 부모 클래스와 그 자식 클래스들은(BlogEntry, Book, 등등) 동일한 테이블을 사용하고 식별 칼럼을 두어 구분한다.

상속 구조당 하나의 테이블(table-per-hierarchy)에 매핑하는 것은 'not null'인 프로퍼티를 가질 수 없다는 단점이 있다. 다른 방법으로 ORM DSL을 이용하여 클래스당 하나의 테이블(table-per-subclass)에 매핑하는 전략을 사용하는 것이다.

그러나 클래스당 하나의 테이블(table-per-subclass)에 매핑하는 전략을 과도하게 사용하면 조인 쿼리가 남발되기 때문에 쿼리 성능이 형편없어 진다. 우리는 상속을 남용하지말고 상속 계층를 너무 깊게 가져가지 말라고 권고한다.

Polymorphic Queries(쿼리의 다형성)

상속은 다형적으로(polymorphically) 질의할 수 있게 해준다. 예를 들어 Content 부모 클래스에서 list 메소드를 사용하면 모든 자식 클래스들이 반환된다:

def content = Content.list() // 블로그 글, 책, 팟 캐스트 모두 나열된다.
content = Content.findAllByAuthor('Joe Bloggs') // 저자를 기준으로 찾는다.

def podCasts = PodCast.list() //팟 캐스트만 나열된다.

5.2.4 Sets, Lists and Maps

Sets of objects(집합)

관계를 만들면 GORM은 기본적으로 java.util.Set을 이용한다. 이 것은 정렬되지 않으며 중복을 허용하지 않는 컬렉션이다. 다음의 Author 클래스가 있다면

class Author {
   static hasMany = [books:Book]
}

GORM은 이 books 프로퍼티를 java.util.Set형식으로 만든다. 컬렉션을 사용할 때 정렬되지 않았다는 것이 문제가 될 수 있다. 정렬된 컬렉션을 사용하길 원한다면 다음처럼 books를 SortedSet으로 명시한다.

class Author {
   SortedSet books
   static hasMany = [books:Book]
}

java.util.SortedSet을 사용하는 경우에는 Book 클래스에 java.lang.Comparable을 구현해야 한다.

class Book implements Comparable {
   String title
   Date releaseDate = new Date()

int compareTo(obj) { releaseDate.compareTo(obj.releaseDate) } }

이 예제대로 라면 Author 클래스의 books 프로퍼티에 Book 객체들이 있게된다. Book 객체들은 releaseDate를 기준으로 정렬될 것이다.

Lists of objects(리스트)

단순하게 집합을 객체가 추가된 순서로 유지하고 배열처럼 인덱스로 객체를 참조하게 하고 싶을 땐 List를 사용하면 된다:

class Author {
   List books
   static hasMany = [books:Book]
}

books 컬렉션의 순서는 객체를 추가한 순서대로 유지된다. 0부터 시작하는 인덱스를 이용하여 다음과 같이 사용할 수 있다:

author.books[0] // 첫 번째 책

데이터베이스 수준에서 일어나는 일을 살펴보면 Hibernate는 books_idx 컬럼을 생성한다. 데이터베이스에서도 순서를 지키기위해서 book_idx에 컬렉션 요소(element)의 인덱스를 저장한다.

List를 사용할 때 요소를 저장하기 전에 컬렉션에 추가해야 한다. 그렇지 않으면 Hibernate는 예외를 던질 것이다(org.hibernate.HibernateException: null index column for collection):

// This won't work!
def book = new Book(title: 'The Shining')
book.save()
author.addToBooks(book)

// Do it this way instead. def book = new Book(title: 'Misery') author.addToBooks(book) author.save()

Maps of Objects(맵)

만약 string/value 쌍만으로 이루어진 단순한 맵을 원하면 다음처럼 사용하면 된다:

class Author {
   Map books // map of ISBN:book names
}

def a = new Author() a.books = ["1590597583":"Grails Book"] a.save()

맵의 키와 값은 모두 반드시 문자열이어야 한다.

객체의 맵을 원한다면 다음처럼 한다.

class Book {
  Map authors
  static hasMany = [authors:Author]
}

def a = new Author(name:"Stephen King")

def book = new Book() book.authors = [stephen:a] book.save()

static hasMany 프로퍼티로 맵의 요소의 형식을 정의할 수 있다. 이 맵의 키는 반드시 문자열이어야 한다.

5.3 Persistence Basics

Grails에서 잊지 말아야 할 것이 있는데 Grails는 내부적으로 Hibernate 을 사용하여 영속성을 구현했다. ActiveRecordiBatis 를 사용한적이 있다면 Hibernate의 "세션 모델"이 조금 어색할 수 있다.

Grails는 자동으로 현재 실행하는 요청을 Hibernate 세션에 바인드(bind)시킨다. 이 것은 우리가 save, delete 등의 GORM 메소드를 투명하게 사용할 수 있게 해준다.

5.3.1 Saving and Updating

다음은 save 메소드를 사용하는 예이다.

def p = Person.get(1)
p.save()

Hibernate와 가장 큰 차이점은 save 메소드를 호출할 때 어떠한 SQL도 실행할 필요가 없다는 것이다. 보통 Hibernate는 SQL 문을 모았다가 끝날때 일괄 처리한다. Grails는 Hibernate Session을 관리해서 자동으로 이 일을 해낸다.

그러나 SQL 문이 언제 실행돼야 하는지를 결정하고 싶을 때도 있다. Hibernate에서는 이 것을 세션을 "flush" 했다라고 한다. 이렇게 하기 위해서 다음처럼 save 메소드를 호출할 때 flush를 인자로 넘길 수 있다.

def p = Person.get(1)
p.save(flush:true)

이전의 save를 포함하여 지연된 모든 SQL 문장들이 DB와 동기화될 것이다. 이 때 예외를 처리할 수 있다. 이 예외처리는 일반적으로 낙관적 잠금(optimistic locking)을 포함하는 고도의 동시적concurrent 시나리오에 유용하다.

def p = Person.get(1)
try {
	p.save(flush:true)
}
catch(Exception e) {
	// deal with exception
}

5.3.2 Deleting Objects

다음은 delete 메소드를 사용하는 예이다.

def p = Person.get(1)
p.delete()

delete 메소드에도 flush 인자를 이용하여 flush시킬 수 있다.

def p = Person.get(1)
p.delete(flush:true)

데이터를 삭제하는 것은 조심해야 하기 때문에 Grails도 deleteAll 메소드를 제공하지 않는다. 이진 flags/logic을 이용해서 데이터를 삭제할 수 있다.

정말 일괄 삭제해야 한다면 executeUpdate 메소드를 이용하여 DML 문을 실행시켜서 삭제할 수 있다.

Customer.executeUpdate("delete Customer c where c.name = :oldName", [oldName:"Fred"])

5.3.3 Understanding Cascading Updates and Deletes

GORM을 사용할 때 연쇄(Cascade) 갱신과 삭제가 어떻게 동작하는 지를 이해하는 것은 중요하다. 클래스가 관계를 "소유"하는 것을 제어하는 belongsTo 설정이 우리가 기억해야 할 핵심이다.

일대일, 일대다, 다대다 관계에 상관없이 belongsTo를 정의했다면 갱신과 삭제는 소유한 클래스에서 그 소유물(관계의 다른 쪽)까지 연쇄적으로 동작할 것이다.

_belongsTo를 정의하지 않으면_ 연쇄 작업은 일어나지 않으며 모든 객체를 일일이 손수 저장해야 한다.

여기에 한 예가 있다.

class Airport {
	String name
	static hasMany = [flights:Flight]
}
class Flight {
	String number
	static belongsTo = [airport:Airport]
}

Airport를 생성하고 Flight를 몇 개 추가한 후 Airport를 저장하면 Flight도 연쇄적으로 저장된다. 결국 생성한 모든 객체가 저장된다.

new Airport(name:"Gatwick")
	 .addToFlights(new Flight(number:"BA3430"))
	 .addToFlights(new Flight(number:"EZ0938"))
	 .save()

반대로 Airport를 삭제하면 관련된 모든 Flight도 삭제될 것이다.

def airport = Airport.findByName("Gatwick")
airport.delete()

그러나 belongsTo를 제거하면 위의 코드는 더 이상 연쇄적으로 삭제하지 않는다. ORM DSL을 사용하여 연쇄 행동을 제어할 수 있다.

5.3.4 Eager and Lazy Fetching

GORM은 기본적으로 Lazy 패칭을 사용한다. 다음 예가 이를 잘 설명한다.

class Airport {
	String name
	static hasMany = [flights:Flight]
}
class Flight {
	String number
	static belongsTo = [airport:Airport]
}

위의 도메인 클래스로 다음과 같이 코드를 작성한다.

def airport = Airport.findByName("Gatwick")
airport.flights.each {
	println it.name
}

GORM은 Airport 인스턴스를 가져오기 위해 단 하나의 SQL을 실행하고 Flight마다 추가로 1개의 쿼리를 더 실행할 것이다. 결국에 N+1번 질의한다.

관계(association)에 드물게 접근하는 경우에는 이 방법이 최적일 것이다. 전적으로 관계(association)의 사용빈도에 달려있다.

다음과 같이 Eager 패칭을 사용하는 것으로 Lazy 패칭의 단점을 해결할 수 있다.

class Airport {
	String name
	static hasMany = [flights:Flight]
	static fetchMode = [flights:"eager"]
}

Airport 인스턴스와 Flight 관계는 매핑 규칙에 따라 한번에 전부 로드될 것이다. 데이터베이스에 질의하는 빈도가 줄어드는 장점이 있지만 Eager Association이 너무 많으면 데이터베이스 전체를 메모리로 로드하게 될 수도 있으니 주의해야 한다.

ORM DSL을 이용하여 Lazy 패치없이 관계를 정의할 수 있다.

5.3.5 Pessimistic and Optimistic Locking

Optimistic Locking(낙관적 잠금)

기본적으로 GORM 클래스는 낙관적 잠금을 사용하도록 돼 있다. 낙관적 잠금은 Hibernate의 것을 이용하므로 데이터베이스의 version 컬럼에 버전을 저장한다.

version 프로퍼티를 통해서 이 버전 컬럼을 읽을 수 있다. 사용하고 있는 영속성 인스턴스의 버전을 읽는다.

def airport = Airport.get(10)

println airport.version

도메인 클래스에 대해서 업데이트를 실행하면 Hiberate는 자동으로 version 프로퍼티과 데이터베이스의 version 컬럼을 검사한다. 만약 서로 다르면 StaleObjectException을 던지고 트랜잭션을 롤백한다.

이 것은 성능 문제를 야기하는 비관적 잠금을 사용하지도 않고서도 원자성을 확실하게 보장하기 때문에 유용하다. 단, 동시 쓰기가 많이 발생한다면 이 예외를 직접 처리해야 한다.

def airport = Airport.get(10)

try { airport.name = "Heathrow" airport.save(flush:true) } catch(org.springframework.dao.OptimisticLockingFailureException e) { // deal with exception }

어플리케이션에 따라 예외를 처리하는 방법이 달라질 수 있다. 전적으로 사용자에게 떠넘길 수도 있고 프로그램으로 데이터를 자동으로 병합해줄 수도 있고 충돌을 해결하도록 사용자에게 요청할 수 있다.

이 방법이 싫다면 비관적 잠금을 사용하면 된다.

Pessimistic Locking(비관적 잠금)

비관적 잠금은 "SELECT * FOR UPDATE" 라는 SQL 문을 실행하는 것과 동일하게 데이터베이스의 특정 열을 잠근다. 잠긴 것이 해제될 때까지 다른 읽기가 블럭된다.

lock 메소드를 이용하여 비관적 잠금을 사용할 수 있다.

def airport = Airport.get(10)
airport.lock() // lock for update
airport.name = "Heathrow"
airport.save()

트랜잭션이 커밋되면 Grails는 잠금을 자동으로 해제한다.

5.4 Querying with GORM

GORM은 동적 파인더(dynamic finder)라는 강력한 질의 방법을 제공한다. 이 것은 Hibernate의 객체지향 쿼리 언어인 HQL에 버금같다.

GPath 와 sort, findAll 같은 메소드들로 컬렉션을 관리할 수 있는 Groovy와 GORM의 결합은 강력한 조합을 만들어 낸다.

그러나. 언제나 천리길도 한 걸음부터.

Listing instances(리스트)

단순하게 list 메소드를 사용하여 클래스의 모든 인스턴스를 조회할 수 있다:

def books = Book.list()

페이지정보(pagination)를 list 메소드의 인자로 넘길 수 있다:

def books = Book.list(offset:10, max:20)

정렬도 가능하다.

def books = Book.list(sort:"title", order:"asc")

sort 인자로 정열할 때 기준이되는 도메인 클래스의 프로퍼티를 명시하고 order 인자에는 오름 정렬일 때 asc를, 내림 정렬일 때에는 desc를 사용한다

Retrieval by Database Identifier(데이터베이스 식별자로 조회)

get 메소드에 데이터베이스 식별자를 인자로 넘겨서 조회할 수 있다.

def book = Book.get(23)

식별자들의 집합을 getAll 메소드의 인자로 넘겨서 인스턴스의 목록을 조회할 수 있다.

def books = Book.getAll(23, 93, 81)

5.4.1 Dynamic Finders

GORM은 동적 파인더 개념을 지원한다. 동적 파인더는 정적 메소드를 실행하는 것과 비슷하다. 그렇지만 동적 파인더를 지원하기 위해 특별한 메소드가 존재하는 것은 아니다.

메소드를 사용하는 것이 아니다. 클래스의 프로퍼티를 기반으로 런타임에 코드를 합성하여 마술처럼 자동으로 생성한다. 다음 Book 클래스의 예를 보자.

class Book {
	String title
	Date releaseDate
	Author author
}                
class Author {
	String name
}

Book 클래스는 title, releaseDate, author같은 프로퍼티를 가지고 있다. 이 프로퍼티들은 '메소드 표현식(method expressions)'에 따라 findBy findAllBy 메소드에서 사용된다.

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

book = Book.findByReleaseDateBetween( firstDate, secondDate )

book = Book.findByReleaseDateGreaterThan( someDate )

book = Book.findByTitleLikeOrReleaseDateLessThan( "%Something%", someDate )

Method Expressions(메소드 표현식)

GORM의 메소드 표현식은 findBy 같은 접두어 뒤에 프로퍼티들을 연결시켜서 완성된다. 기본식은 다음과 같다.

Book.findBy[Property][Suffix]*[Boolean Operator]*[Property][Suffix]

*는 생략가능하고 각각의 접미어에 따라 쿼리가 달라진다. 예를 들면:

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

이 예에서 전자는 등호 연산과 동일하고 후자는 접미어 Like로 인해 like 연산으로 동작한다.

가능한 접미어들:

You'll notice the last 3 effect the number of arguments required to the method as demonstrated by the example:

def now = new Date()
def lastWeek = now - 7
def book = Book.findByReleaseDateBetween( lastWeek, now )

Equally isNull and isNotNull require no arguments:

def books = Book.findAllByReleaseDateIsNull()

Boolean logic (AND/OR)

Method expressions can also use a boolean operator to combine two criteria:

def books = 
    Book.findAllByTitleLikeAndReleaseDateGreaterThan("%Java%", new Date()-30)

In this case we're using And in the middle of the query to make sure both conditions are satisfied, but you could equally use Or:

def books = 
    Book.findAllByTitleLikeOrReleaseDateGreaterThan("%Java%", new Date()-30)

Clearly, method names can end up being quite long, in which case you should consider using Criteria.

Querying Associations

Associations can also be used within queries:

def author = Author.findByName("Stephen King")

def books = author ? Book.findAllByAuthor(author) : []

In this case if the Author instance is not null we use it in a query to obtain all the Book instances for the given Author.

Pagination & Sorting

The same pagination and sorting parameters available on the list method can also be used with dynamic finders by supplying a map as the final parameter:

def books = 
  Book.findAllByTitleLike("Harry Pot%", [max:3, 
                                         offset:2, 
                                         sort:"title",
                                         order:"desc"])

5.4.2 Criteria

Criteria는 복잡한 쿼리를 만들기 위해 Groovy 빌더를 사용한다. Criteria는 자료형이 보장(type safe)되는 매우 훌륭한 도구다. 이 것이 StringBuffer보다 훨씬 훌륭하다.

createCriteriawithCriteria 메소드를 통해서 Criteria를 사용한다. 이 빌더는 Hibernate의 Criteria API를 사용한다. 빌더의 노드들은 Hibernate의 Criteria API의 Restrictions 클래스에 있는 static method들에 매핑된다. 다음의 예를 보자:

def c = Account.createCriteria()
def results = c {
	like("holderFirstName", "Fred%")
	and {
		between("balance", 500, 1000)
		eq("branch", "London")
	}
	maxResults(10)
	order("holderLastName", "desc")
}

Conjunctions and Disjunctions(논리곱과 논리합)

위의 예에서 보여주듯이 and { }을 이용하여 논리블럭을 만들고 Criteria에 논리연산을 적용할 수 있다:

and {
	between("balance", 500, 1000)
	eq("branch", "London")
}

OR 연산자도 마찬가지다:

or {
	between("balance", 500, 1000)
	eq("branch", "London")
}

NOT 연산자에서도 잘 동작한다:

not {
	between("balance", 500, 1000)
	eq("branch", "London")
}

Querying Associations(관계 질의하기)

프로퍼티에 알 맞는 노드가 있으면 관계를 질의할 수 있다. 다음 예제는 Account 클래스는 많은 Transaction 객체를 가질 수 있다는 것을 말해준다.

class Account {
    …
    def hasMany = [transactions:Transaction]
    Set transactions
    …
}

우리는 빌더 노드에 transaction 프로퍼티을 사용하여 이 관계를 질의할 수 있다.

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
       transactions {
            between('date',now-10, now)
       }
}

이 예제는 최근 10일안에 transaction이 있었던 Account 인스턴스를 모두 찾는다. 그리고 그러한 관계를 질의하는 쿼리를 논리 블럭안에 넣을 수 있다.:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
     or {
        between('created',now-10,now)
        transactions {
             between('date',now-10, now)
        }
     }
}

이 예제는 최근 10일 이내에 transaction이 수행됐었거나 생성된 Account 인스턴스들은 모두 찾는다.

Querying with Projections(프로젝션으로 질의하기)

프로젝션은 결과를 사용자 사정에 맞추는데(customize) 사용된다. 프로젝션을 사용하기 위해서는 Criteria 빌더 트리에 "projections" 노드를 정의해야 한다. projections 노드의 메소드은 Hibernate의 Projections 클래스의 메소드와 동일하다:

def c = Account.createCriteria()

def numberOfBranches = c.get { projections { countDistinct('branch') } }

Using Scrollable Results(스크롤되는 결과의 사용)

scroll 메소드를 호출하여 Hiberate의 ScrollableResults 기능을 사용할 수 있다.

def results = crit.scroll {
      maxResults(10)
}
def f = results.first()
def l = results.last()
def n = results.next()
def p = results.previous()

def future = results.scroll(10) def accountNumber = results.getLong('number')

Hiberate에서 ScrollableResult를 설명하는 문서를 인용했다:

원하는 개수만큼 결과를 스크롤할 수 있는 result iterator이다. Query/ScrollableResults 패턴은 JDBC PreparedStatement/ResultSet 패턴과 매우 유사하고 메소드의 이름도 ResultSet의 것과 비슷하게 지어졌다.

하지만 JDBC와는 다르게 결과의 컬럼의 인덱스는 0부터 시작한다.

Setting properties in the Criteria instance(Criteria 인스턴스의 프로퍼티를 설정하기)

빌더 트리에서 기술한 조건이 해석할 수 없으면 Criteria 인스턴스의 자체 프로퍼티를 설정하려고 시도한다. 그래서 이 클래스의 모든 프로퍼티에 접근하는 것이 가능하다. 아래의 예를 보면 Criteria 인스턴스의 setMaxResultssetFirstResult 메소드가 호출된다.

import org.hibernate.FetchMode as FM
	…
	def results = c.list {
		maxResults(10)
		firstResult(50)
		fetchMode("aRelationship", FM.EAGER)
	}

Querying with Eager Fetching(Eager 패칭으로 질의하기)

Eager and Lazy Fetching 을 설명하면서 우리는 N+1 select 문제를 회피하는 방법에 대해 이미 다루었다. 하지만 Criteria를 이용하여 동일한 일을 할 수 있다:

import org.hibernate.FetchMode as FM
...

def criteria = Task.createCriteria() def tasks = criteria.list{ eq("assignee.id", task.assignee.id) fetchMode('assignee', FM.EAGER) fetchMode('project', FM.EAGER) order('priority', 'asc') }

Method Reference(메소드 레퍼런스)

만약 다음의 예제처럼 아무것도 없이 빌더를 실행하면:

c { … }

결과의 목록을 얻어오는 것이 빌더의 목적이므로 다음의 예제와 동일하게 작동된다:

c.list { … }

메소드설명
list기본 메소드로 조건에 만족하는 모든 열을 반환한다.
get단 한 개의 결과 집합(result set)를 반환한다. 이 메소드를 위한 Criteria는 단 한 개의 결과를 반환하도록 만들어져야 한다. 이 메소드와 단지 첫 열만을 얻어오는 것과 혼동하지 말아야 한다.
scroll스크롤되는 결과를 반환한다.
listDistinct서브쿼리나 관계를 이용할 때 결과 집합에서 동일한 열이 여러개 존재할 수 있는데 이 것으로 중복을 허용하지 않을 수 있다. CriteriaSpecification 클래스의 DISTINCT_ROOT_ENTITY와 동일하다.

5.4.3 Hibernate Query Language (HQL)

GORM에서는 HQL도 사용할 수 있다. Hiberate 문서의 14장 HQL: The Hibernate Query Language에서 HQL에대한 모든 것을 참고할 수 있다.

GORM은 find, findAll executeQuery 등의 HQL을 사용할 수 있는 얼마간의 메소드들을 제공한다.

def results =
      Book.findAll("from Book as b where b.title like 'Lord of the%'")

Positional and Named Parameters(위치 파라미터와 이름 파라미터)

쿼리에 필요한 값은 하드코딩하려 한다면 위치 파리미터를 사용할 수 있다:

def results =
      Book.findAll("from Book as b where b.title like ?", ["The Shi%"])

이름 파라미터를 사용하는 것도 가능하다:

def results =
      Book.findAll("from Book as b where b.title like :search or b.author like :search", [search:"The Shi%"])

Multiline Queries(여러 줄로 질의하기)

쿼리를 여러줄에 걸쳐서 만들어야 한다면 라인연결문자를 사용하라:

여러 줄 문자열을 만들기 위한 Groovy로 표현법은 HQL 쿼리에 사용할 수 없다.

Pagination and Sorting(페이지 매김과 정렬)

HQL에서도 패이지를 매길 수 있고 정열할 수 있다. 간단하게 메소드 끝에 맵 형식으로 페이지 매김과 정열 옵션을 기술한다.

def results =
      Book.findAll("from Book as b where b.title like 'Lord of the%'", 
                   [max:10, offset:20, sort:"title", order:"asc"])

5.5 Advanced GORM Features

앞으로 우리는 캐싱, 매핑 방법, 이벤트를 다루는 방법들에 대해 알아보자.

5.5.1 Events and Auto Timestamping

GORM은 delete, insert, update같은 이벤트가 발생할 때 수행될 클로저(closure)를 등록할 수 있다. 단순히 도메인 클래스의 이벤트에 적절한 클로저를 동록하면 된다. 다음과 같은 이벤트들이 있다.

The beforeInsert event(beforeInsert 이벤트)

객체가 데이터베이스에 저장되기 전에 실행된다.

class Person {
   Date dateCreated

def beforeInsert = { dateCreated = new Date() } }

The beforeUpdate event(beforeUpdate 이벤트)

객체가 업데이트되기 전에 실행된다.

class Person {
   Date dateCreated
   Date lastUpdated

def beforeInsert = { dateCreated = new Date() } def beforeUpdate = { lastUpdated = new Date() } }

The beforeDelete event(beforeDelete 이벤트)

객체가 삭제되기 전에 실행된다.

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def beforeDelete = { new ActivityTrace(eventName:"Person Deleted",data:name).save() } }

The onLoad event(onLoad 이벤트)

데이터베이스에서 객체가 로드될때 실행된다:

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def onLoad = { name = "I'm loaded" } }

Automatic timestamping(자동 시간도장)

위의 예에서 이벤트를 사용하여 lastUpdateddateCreated 프로퍼티를 업데이트하는 방법에 대해 알아보았다. GORM은 객체의 히스토리를 유지하는 다른방법을 제공한다. GORM에서는 단순히 lastUpdateddateCreated 프로퍼티만 정의하는 것만으로도 충분한다.

이 기능이 맘에 들지 않는다면 끌 수 있다:

class Person {
   Date dateCreated
   Date lastUpdated
   static mapping = {
      autoTimestamp false
   }
}

5.5.2 Custom ORM Mapping

Grails의 도메인 클래스는 ORM DSL(Object Relational Mapping Domain Specify Language) 레거시 스키마에 매핑될 수 있다. 다음 장에서 ORM DSL로 할 수 있는 일들을 설명한다.

GORM의 관례에 따르는 테이블, 컬럼 이름등이 마음에 든다면 이 것은 전혀 필요없다. 캐싱을 한다거나 GORM을 레거시 스키마에 매핑하려 할때에만 이 기능이 필요하다.

도메인 클래스에 정적 mapping 블럭을 사용하므로써 매핑 규칙을 정의할 수 있다:

class Person {
  ..
  static mapping = {

} }

5.5.2.1 Table and Column Names

Table names(테이블 이름)

table을 사용하여 클래스가 매핑될 데이터베이스 테이블 이름을 정의할 수 있다.:

class Person {
  ..
  static mapping = {
      table 'people'
  }
}

이 경우 person이 아니라 people이라는 테이블에 매핑될 것이다.

Column names(컬럼 이름)

데이터베이스의 컬럼에 매핑하는 규칙도 정의할 수 있다. 다음의 예제처럼 원하는 이름으로 매핑시킬 수 있다:

class Person {
  String firstName
  static mapping = {
      table 'people'
      firstName column:'First_Name'
  }
}

이 예제에서는 매핑할 프로퍼티 이름은 firstName이고 column이라는 파라미터를 이용하여 어느 컬럼에 매핑할지 명시한다.

Column type(컬럼 타입)

GORM은 DSL의 타입 속성(attribute)으로 Hibernate 타입을 설정할 수 있다. 이 것은 org.hibernate.types.UserType를 상속한 사용자 타입도 명시할 수 있고 타입에 어떻게 영속성을 부여할지 정의할 수 있게 한다. PostCodeType을 만들었고 이 것을 사용한다면 다음의 예제처럼 할 수 있다:

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:PostCodeType
   }
}

뿐만아니라 Grails가 선택하는 기본 타입을 사용하지 않고 Hibernate가 제공하는 기본 타입중에 하나로 매핑하게 할 수 있다:

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:'text'
   }
}

이 예제에서는 postCode 컬럼이 SQL TEXT나 CLOB 형식에 매핑된다. 매핑되는 형식은 사용하는 데이터베이스에 따라 다르다.

One-to-One Mapping(일대일 매핑)

관계(association)가 있을 때 관계를 매핑하는 외래 키를 변경하는 것도 가능하다. 일대일 관계에서는 일반 컬럼을 매핑하는 것과 동일하다. 예를 들면 다음과 같다:

class Person {
  String firstName
  Address address
  static mapping = {
      table 'people'
      firstName column:'First_Name'
	  address column:'Person_Adress_Id'
  }
}

기본적으로 address 관계는 외래키 컬럼 address_id에 매핑된다. 그러나 Person_Adress_Id 컬럼에 매핑하도록 변경했다.

One-to-Many Mapping(일대다 매핑)

양방향 일대다 관계에서는 '다'쪽의 컬럼이름을 변경하는 것만으로도 외래키 컬럼을 변경할 수 있다. 하지만 단방향 관계에서는 관계 자체에 외래키를 명시해야 한다. 다음은 PersonAddress사이의 단방향 일대다 관계에서 address 테이블의 외래키를 변경시키는 예제이다:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      firstName column:'First_Name'
	  addresses column:'Person_Address_Id'
  }
}

joinTable 파라미터를 이용하면 address 테이블에 있는 컬럼이 아니라 테이블을 조인할 수 있다:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      firstName column:'First_Name'
      addresses joinTable:[name:'Person_Addresses', key:'Person_Id', column:'Address_Id']
  }
}

Many-to-Many Mapping(다대다 매핑)

Grails에서는 기본적으로 n-n 관계를 조인 테이블으로 매핑한다. 다음은 n-n 관계의 예제이다:

class Group {
	…
	static hasMany = [people:Person]
}
class Person {
	…
	static belongsTo = Group
	static hasMany = [groups:Group]
}

이 예제에서 Grails는 persongroup 테이블을 참조하는 외래키 person_idgroup_id을 사용하여 group_person이라는 조인 테이블을 만든다. 각 클래스를 매핑할 때 mapping 블럭에 컬럼을 명시하여 매핑하는 컬럼도 변경할 수 있다:

class Group {
   …
   static mapping = {
       people column:'Group_Person_Id'
   }	
}
class Person {
   …
   static mapping = {
       groups column:'Group_Group_Id'
   }	
}

사용할 조인 테이블의 이름도 명시할 수 있다:

class Group {
   …
   static mapping = {
       people column:'Group_Person_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
   }	
}
class Person {
   …
   static mapping = {
       groups column:'Group_Group_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
   }	
}

5.5.2.2 Caching Strategy

Setting up caching(캐싱 설정하기)

Hibernate 는 사용자가 정의 가능한 캐시 프로바이더(cache provider)를 지원하는 2차 캐시 second-level cache를 가지고 있다. 다음의 예처럼 grails-app/conf/DataSource.groovy에 설정한다:

hibernate {
    cache.use_second_level_cache=true
    cache.use_query_cache=true
    cache.provider_class='org.hibernate.cache.EhCacheProvider'
}

당연히 원하는 대로 설정할 수 있다. 예를 들어, 분산 캐시 매커니즘을 사용하고 싶다면 그렇게 할 수 있다.

캐시에 대하여, 특히 Hibernate의 2차 캐시second-level cache에 대하여 더 알고 싶으면 Hibernate 문서에서 관련 주제를 참고하라.

Caching instances(인스턴스 캐싱하기)

기본적으로 캐싱되게 하려면 mapping 블럭에 cache 매소드를 호출한다:

class Person {
  ..
  static mapping = {
      table 'people'
      cache true
  }
}

이 예제에서는 lazy든 아니든 상관하지 않고 'read-write'로 캐싱한다. 물론 이 전략도 변경할 수 있다:

class Person {
  ..
  static mapping = {
      table 'people'
      cache usage:'read-only', include:'non-lazy'
  }
}

Caching associations(관계 캐싱하기)

인스턴스를 캐싱할 때 Hibernate의 2차 캐시를 사용하도록 할 수 있을 뿐만 아니라 객체의 컬렉션도 캐싱할 수 있다. 다음의 코드를 보면:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      version false
      addresses column:'Address', cache:true
  }
}
class Address {
   String number
   String postCode
}

이 예제는 addresses 컬렉션에 대해 'read-write'로 캐싱한다. 'read-write'뿐만 아니라 다른 것도 사용할 수 있다:

cache:'read-write' // or 'read-only' or 'transactional'

캐시 사용법에서 보다 자세히 설명한다.

Cache usages(캐시 사용법)

다음은 설정할 수 있는 캐싱 정책과 그 사용법에 대한 설명이다.

5.5.2.3 Inheritance Strategies

GORM은 기본적으로 상속 구조당 하나의 테이블(table-per-hierarchy)로 매핑시킨다. 이것은 데이터베이스의 컬럼이 NOT-NULL 제약조건을 따르도록 하지 못하는 단점이 있다. 클래스당 하나의 테이블(table-per-subclass)을 사용하는 전략을 취하려면 다음처럼 한다:

class Payment {
    Long id
    Long version
    Integer amount

static mapping = { tablePerHierarchy false } } class CreditCardPayment extends Payment { String cardNumber }

기본 클래스인 Payment를 상속받은 모든 클래스는 상속 구조당 하나의 테이블(table-per-hierarchy)로 매핑되지 않을 것이다.

5.5.2.4 Custom Database Identity

DSL을 사용하여 GORM이 데이터베이스 식별자를 생성하는 방법을 정의할 수 있다. GORM은 사용하는 데이터베이스가 메커니즘에 따라 id를 생성한다. 이 것은 분명 최상의 방법인데 아직도 다른 방법으로 접근해야 하는 스키마들이 많이 있다.

Hibernate의 식별자 생성기id generator를 정의해야 한다. 입맛에 맞는 식별자 생성기(id generator)를 정의하고 그에 따라 매핑되게 할 수 있다.

class Person {
  ..
  static mapping = {
      table 'people'
      version false
      id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100]
  }
}

이 예에서는 Hibernate에 기본적으로 포함된 'hilo' 생성기를 사용한다. 'hilo' 생성기는 식별자를 생성하기 위해 별도의 테이블을 사용한다.

Hibernate 생성기에 대한 정보가 더 필요하면 Hibernate reference documentation를 참고하라

식별자가 사용하는 컬럼을 명시하려면 다음과 같이 할 수 있다:

class Person {
  ..
  static mapping = {
      table 'people'
      version false
      id column:'person_id'
  }
}

5.5.2.5 Composite Primary Keys

GORM은 두 개 이상의 속성으로 구성되는 복합 식별자의 개념을 지원한다. 권장되는 방법이 아니지만 할 수 있다:

class Person {
  String firstName
  String lastName

static mapping = { id composite:['firstName', 'lastName'] } }

이 예는 Person클래스의 firstNamelastName 속성을 이용하는 복합 식별자를 만든다. 나중에 식별자로 인스턴스를 얻어와야 한다면 다음처럼 객체의 프로토타입을 이용해야 한다:

def p = Person.get(new Person(firstName:"Fred", lastName:"Flintstone"))
println p.firstName

5.5.2.6 Database Indices

우리는 종종 쿼리를 최적화 하기위해 테이블 인덱스를 사용해야 한다. 어떻게 사용해야 하는 가는 문제 도메인에 따라 다르고 쿼리가 사용되는 패턴에 따라 다르다. GORM의 DSL로 컬럼을 어떤 인덱스에 태워야 할지를 명시 할 수 있다:

class Person {
  String firstName
  String address
  static mapping = {
      table 'people'
      version false
      id column:'person_id'
      firstName column:'First_Name', index:'Name_Idx'
      address column:'Address', index:'Name_Idx, Address_Index'
  }
}

5.5.2.7 Optimistic Locking and Versioning

낙관적 잠금과 비관적 잠금에 대해 이미 살펴보았듯이 기본적으로 GORM은 낙관적 작금을 사용하고 자동으로 version 속성을 모든 클래스에 주입(inject)한다. 그리고 이 version 속성은 데이터베이스의 version 컬럼에 매핑된다.

레거시 스키마에 이대로 매핑한다면 골칫거리가 될 뿐이다. 레거시 스키마에 매핑할 때에는 이 기능을 끌 수 있다:

class Person {
  ..
  static mapping = {
      table 'people'
      version false
  }
}

낙관적 잠금을 사용하지 않으면 근본적으로 동시 업데이트를 고려해야 한다. 그리고 비관적 잠금을 사용하지 않으면 사용자가 데이터를 덮어 쓸수도 있기 때문에 언제라도 데이터를 잃어 버릴 수 있다.

5.5.2.8 Eager and Lazy Fetching

Lazy Collections(Lazy 컬렉션)

Eager 패칭과 Lazy 패칭에서 이미 살펴봤듯이 GORM은 기본적으로 컬렉션에대해 lazy 패칭을 사용한다. 이 것은 fetchMode 설정으로 변경할 수 있다. 그러나 ORM DSL을 이용하여 모든 매핑 설정을 한데 모을 수 있다:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      addresses lazy:false
  }
}
class Address {
  String street
  String postCode
}

Lazy Single-Ended Associations(한 쪽의 관계에서의 Lazy 패칭)

GORM에서 1-1 과 n-1 association은 기본적으로 lazy 패칭이 아니다. 다른 엔터티와의 관계가 많은 엔터티를 로드하는 경우에 골칫거리가 될 수 있다. 엔터티를 로드할 때마다 새로운 SELECT 문이 수행되기 때문에 문제가 된다. 1-n, n-n association에서 lazy 컬렉션을 이용하여 lazy 패칭을 하게 할 수 있다:

class Person {
	String firstName
	static belongsTo = [address:Address]
	static mapping = {
		address lazy:true // lazily fetch the address
	}
}
class Address {
	String street
	String postCode
}

Person 클래스의 address 속성이 필요할 때(lazily) Load되도록 설정했다.

5.6 Programmatic Transactions

Grails는 Spring을 기반으로 만들었고 Spring의 Transaction 추상화 기술을 사용하여 트랜젝션 프로그래밍을 지원한다. GORM은 클래스에 withTransaction 메소드를 추가하여 사용하기 쉽도록 개선했다. withTransaction 메소드는 첫 번째 인자로 Spring의 TransactionStatus 객체를 넘겨 받는다.

일반적인 사용법은 다음과 같다:

def transferFunds = {
	Account.withTransaction { status ->
		def source = Account.get(params.from)
		def dest = Account.get(params.to)

def amount = params.amount.toInteger() if(source.active) { source.balance -= amount if(dest.active) { dest.amount += amount } else { status.setRollbackOnly() } }

}

}

이 예제에서 dest 계좌가 active가 아니면 트랜젝션이 롤백되고 트랜젝션이 처리되는 중간에 예외가 발생하면 자동으로 롤백된다.

트랜젝션이 통째로 롤백되지 않고 특정 시점으로 트랜젝션이 롤백되도록 “save points”를 사용할 수 있다. 이 것은 Spring의 SavePointManager 인터페이스를 사용하여 구현됐다.

withTransaction 메소드의 블럭내에서만 begin/commit/rollback 로직을 수행할 수 있다.

5.7 GORM and Constraints

유효성 검사(Validation)에서도 제약조건이 언급되지만 어떤 제약조건은 테이터베이스 스키마를 생성하는 방법에 영향을 준다. 그래서 여기에서도 검토할 필요가 있다.

운 좋게도 Grails의 제약조건은 도메인 클래스의 속성과 관련된 데이터베이스 컬럼을 생성하는 방법에 관여한다.

다음 예를 보자. 다음 속성을 갖는 도메인 모델을 만들었다.

String name
String description

MySQL의 경우에 기본적으로 GORM은 이 컬럼을 다음과 같이 정의한다.

컬럼 이름 | 데이터 타입
 description | varchar(255)

그러나 도메인 클래스의 비지니스 로직은 description의 길이가 1000자가 될 수도 있다. 이 경우에 SQL로 테이블을 생성한다면 다음처럼 할 것이다.

컬럼 이름 | 데이터 타입 
 description | TEXT

데이터를 저장하기 전에 어플리케이션에서 1000자를 초과하지 못하도록 하는 유효성 검사를 했으면 좋겠다. Grails에서는 constraints로 이 일을 할 수 있다. 도메인 클래스에 다음과 같이 코드를 추가한다:

static constraints = {
        description(maxSize:1000)
}

이 제약조건은 어플리케이션에서 유효성을 검사하도록 할뿐만 아니라 위에서 설명한 대로 스키마를 생성한다. 이제부터는 스키마 생성에 관여하는 다른 제약조건들을 살펴볼 것이다.

Constraints Affecting String Properties(문자열 속성에 관여하는 제약조건들)

maxSizesize 제약조건이 있으면 Grails는 제약조건의 값을 참조하여 최대 컬럼 길이를 결정한다.

일반적으로 동일한 도메인 클래스의 속성에 이 두개의 제약조건 동시에 사용하는 것을 권장하지 않는다. 그러나 maxSize 제약조건과 size 제약조건이 둘 다 정의되면 Grails는 maxSize 제약조건의 값과 size 제약조건의 상한 값중에서 작은 수에 따라 컬럼의 길이를 결정한다(이 값을 초과하면 유효성 검사에서 에러를 발생시키기 때문에 Grails는 두 값 중에서 최소값을 사용한다).

inList 제약조건이 정의되면(그리고 maxSizesize 제약조건이 정의되어있지 않다면) Grails는 유요한 값 중에서 가장 긴 문자열의 길이를 컬럼의 최대 길이로 설정한다. 예를 들어, "Java", "Groovy", "C++"들이 있을 때 Grails는 컬럼의 길이를 6으로 설정한다("Groovy" 문자열의 길이가 6이다).

Constraints Affecting Numeric Properties(숫자 속성에 관여하는 제약조건들)

max 제약조건, min 제약조건, range 제약조건이 정의되면 Grails는 이 제약조건들에 따라 컬럼의 전체 자릿수(precision)를 결정한다. 이 제약조건의 적용 여부는 Hibernate가 사용하는 DBMS를 어떻게 연동되는 가에 달려있다.

일반적으로 동일한 도메인 클래스의 속성에 min/max 제약조건과 range 제약조건을 동시에 사용하는 것을 권장하지 않는다. 만약 이 제약조건이 동시에 사용되면 Grails는 제약조건중 가장 작은 값을 사용한다(최소 전체 자릿수precision를 벗어나는 값은 유효성 검사에서 에러를 발생시키기 때문에 Grails는 두 개중 작은 값을 사용한다).

scale 제약조건이 정의되면 Grails는 이 제약조건의 값에 따라 컬럼의 소수 자릿수(scale)를 결정한다(예를 들면, java.lang.Float, java.Lang.Double, java.lang.BigDecimal, or subclasses of java.lang.BigDecimal). 이 제약조건의 적용 여부는 Hibernate가 사용하는 DBMS를 어떻게 연동되는 가에 달려있다.

제약조건에 최대, 최소 값을 정의하면 Grails는 전체 자리수(precision)에 사용할 최대 값을 산출해낸다. 'max:100'만 설정되면 매우 큰 음수가 있을 수 있기 때문에 min/max 제약조건은 둘 다 설정되지 않으면 스키마 생성에 관여하지 않는다는 것을 잊지마라. 제약조건의 값이 Hibernate 컬럼의 전체 자릿수(현재는 기본값이 19다)보다 크지 않으면 Hibernate의 기본값이 사용된다. 예를 들면:

someFloatValue(max:1000000, scale:3)

이 것은 다음과 같은 의미이다:

someFloatValue DECIMAL(19, 3) // 기본 전체 자릿수(precision)가 적용됐다.

그러나 다음과 같은 경우엔:

someFloatValue(max:12345678901234567890, scale:5)

다음과 같은 의미를 가진다:

someFloatValue DECIMAL(25, 5) // 전체 자릿수(precision) = max 값의 자릿수 + 소수 자릿수(scale)

또 다음처럼 min, max 제약조건이 동시에 사용하는 경우에는:

someFloatValue(max:100, min:-100000)

다음과 같은 의미를 가진다:

someFloatValue DECIMAL(8, 2) // 전체 자릿수(precision) = min 값의 자릿수 + 기본 소수자릿수(scale:2)